package dreic

class Analyzer {
    val verbose: boolean = false
    var csc: ClassScope = new ClassScope;

    // Analyze a program
    def analyzeProgram(tree: Program): Unit = {
        analyzeClasses(tree.classes)
        analyzeMembers(tree.classes)
        analyzeMethodBodies(tree.classes)
        analyzeStatement(tree.statement, new VarScope());
    }

    def analyzeClasses(classes: List[ClassDef]): Unit = {
        classes match{
            case x::xs => analyzeClass(x, new FieldScope, new MethodScope); analyzeClasses(xs);
            case Nil =>
        }
    }

    def analyzeClass(clazz: ClassDef, fsc: FieldScope, msc: MethodScope): Unit = {
        if (verbose) Console.println("analyzeClass: " + clazz.name)
        var parents : List[Name] = Nil
        clazz.name2 match {
            case Some(a) => {
                csc.lookup(a) match {
                    case Some(b) => {
                        parents = b.parents:::List(a)
                    }
                    case None => {
                        Report.error(clazz.pos, "Superclass " + a + " not found (info: you can only extend classes after they are defined)");
                    }
                }
            }
            case None =>
        }
        
        csc.lookup(clazz.name) match {
            case Some(a) => Report.error(clazz.pos, "ClassDecl: class " + clazz.name + " already exists")
            case None => csc.enter(ClassSymbol(clazz.name, parents, fsc, msc, clazz.pos));
        }
    }
        
    def analyzeMembers(classDefs: List[ClassDef]): Unit = {
        var classesIterator = classDefs.elements
        while (classesIterator.hasNext) {
            var clazz = classesIterator.next
            var fsc = csc.lookup(clazz.name) match {
                case Some(a) => a.fields
            }
            var msc = csc.lookup(clazz.name) match {
                case Some(a) => a.methods
            }
            var membersIterator = clazz.membres.elements
            while (membersIterator.hasNext) {
                var m = membersIterator.next
                m match {
                    case f: FieldDecl => analyzeField(f, fsc, clazz)
                    case method: MethodDef => analyzeMethod(method, msc, clazz)
                }
            }
        }
    }
    
    def analyzeField(f: FieldDecl, fsc: FieldScope, clazz: ClassDef) {
        if (verbose) Console.println("analyzeField: " + f.name)
        
        // inheritance check: avoid field redefinition in extending classes [DONE]
        clazz.name2 match {
            case Some(parentClass) => {  // this class inherits from a superclass, check whether a field with that name is already defined
                csc.lookup(parentClass) match {
                    case Some(parentClassSymbol) => {
                        var fsc: FieldScope = parentClassSymbol.fields
                        fsc.lookup(f.name) match {
                            case Some(field) => {
                                Report.error(f.pos, "field '" + f.name + "' is already defined in the superclass '" + parentClass + "'")
                            }
                            case None => {  // this class inherits, but there is no field defined yet with that name
                                var t: Type = analyzeType(f.field_type)
                                if (t!=null){
                                    if (verbose) Console.println("    Entering new field: " + f.name)
                                    fsc.lookup(f.name) match {
                                        case Some(a) => Report.error(a.pos, "Field " + a.name + " already defined." + a.pos)
                                        case None => fsc.enter(new FieldSymbol(f.name, t, f.pos, 0));
                                    }
                                }
                            }
                        }
                    }
                    case None =>
                }
            }
            case None => {  // there is no inheritance, so the field gets defined
                var t: Type = analyzeType(f.field_type)
                if (t!=null){
                    if (verbose) Console.println("    Entering new field: " + f.name)
                    fsc.lookup(f.name) match {
                        case Some(f) => Report.error(f.pos, "Field " + f.name + " already defined.")
                        case None => fsc.enter(new FieldSymbol(f.name, t, f.pos, 0));
                    }
                }
            }
        }
    }

    def analyzeMethod(method: MethodDef, msc: MethodScope, clazz: ClassDef): Unit = {
        // 1. for each argument, its type has to exist. [DONE]
        // 2. the return value type has to exist. [DONE]
        // 3. if this method definition overrides another one, verify that the arguments and return types are correctly typed [DONE]
        // 4. insert this and the parameters into the variable environment of this method [DONE]
        
        // case class MethodDef(name: Name, args: List[Pair[Name, TypeTree]], ret_type: Option[TypeTree], body : Expr) extends Member;
        // var methodScope: ListMap[Name, MethodSymbol] = new ListMap
        // case class MethodSymbol(name: Name, argTypes: List[Type], retType: Type, pos: Int) extends Symbol
        
        if (verbose) Console.println("analyzeMethod: " + method.name)
        
        // check whether we are overriding an inherited method
        clazz.name2 match {
            case Some(parentClass) => {  // inheritance present
                csc.lookup(parentClass) match {
                    case Some(parentClassSymbol) => {
                        var msc_parent: MethodScope = parentClassSymbol.methods
                        msc_parent.lookup(method.name) match {  // check if a method of the same name is already present
                            case Some(parentMethod) => {
                                if (verbose) Console.println("    Overriding existing method " + parentMethod.name)
                                
                                // verify existence of the arguments' types and create an arguments types list
                                var argsIterator = method.args.elements
                                val argTypes: List[Type] = method.args.map(._2).map(analyzeType)

                                // verify the existence of the return type
                                if (verbose) Console.println("    Analyzing return type")
                                var retType: Type = method.ret_type match {
                                    case Some(rt) => analyzeType(rt)
                                    case None => null
                                }
                                
                                // type check against superclass [DONE]: 
                                // 1. Arguments have to be supertypes of parent argument types [DONE]
                                // 2. Return type has to be subtype of parent return type [DONE]
                                
                                // check whether number of arguments are the same
                                if (parentMethod.argTypes.length != argTypes.length) Report.error(method.pos, "Number of arguments in the overriding method does not correspond to the number of arguments in the overridden method.")
                                else { // check whether the argument types agree
																	if (List.forall2(parentMethod.argTypes, argTypes)((a,b)=>isSubType(b,a))){
																    if (isSubType(retType, parentMethod.retType)){
																      var vsc: VarScope = new VarScope
																      vsc.enter(new VarSymbol(Name("this"), csc.lookup(clazz.name) match {case Some(a) => IClassType(a);case None => INoType}, method.pos, 0))
																      argsIterator = method.args.elements
																      var argTypesIterator = argTypes.elements
																      while (argsIterator.hasNext) {
																        var arg = argsIterator.next
																	vsc.enter(new VarSymbol(arg._1, argTypesIterator.next, method.pos, 0))
																      }
																    }else{
																      Report.error(method.pos, "Method override: return types do not agree. The return type of the overriding method has to be a subtype of the overridden method.")
																    }
																  }else{
																    Report.error(method.pos, "Method override: parameter types do not agree. The overriding arguments' parameters have to be supertypes of the overridden parameters.")
																  }
                                }
                            }
                            case None => { //inheritence present + method doesn't exist in parent class
															msc.lookup(method.name) match {
																case None => {
									                if (verbose) Console.println("    Defining method " + method.name)
									                // verify the existence of the arguments' types and construct the argTypes list
									                //var argsIterator = method.args.elements
									                val argTypes: List[Type] = method.args.map(._2).map(analyzeType)
									            
									                // verify the existence of the return type
									                if (verbose) Console.println("    Analyzing return type")
									                var retType: Type = method.ret_type match {
									                    case Some(rt) => analyzeType(rt)
									                    case None => null
									                }
									                
									                // type check finished, insert variables into variable scope and method into method scope
									                // case class VarSymbol(name:Name, t: Type, pos: Int) extends Symbol
									                var vsc: VarScope = new VarScope
									                vsc.enter(new VarSymbol(Name("this"), csc.lookup(clazz.name) match {case Some(a) => IClassType(a);case None => INoType}, method.pos, 0))
									                

									                var argsIterator = method.args.elements
									                var argTypesIterator = argTypes.elements
									                while (argsIterator.hasNext) {
									                    var arg = argsIterator.next
									                    vsc.enter(new VarSymbol(arg._1, argTypesIterator.next, method.pos, 0))
									                }
									                var msc: MethodScope = csc.lookup(clazz.name) match {
									                    case Some(c) => c.methods
									                    case None => new MethodScope()
									                }
									                if (verbose) Console.println("    Entering method " + clazz.name + "." + method.name)
									                if (msc != null) msc.enter(new MethodSymbol(method.name, argTypes, retType, vsc, method.pos))
																}
																case Some(a) => {
																	Report.error(a.pos, "Method " + a.name + " already defined in the same class");
																}
															}
/*
                                if (verbose) Console.println("    Defining new method " + method.name)
                                var argsIterator = method.args.elements
                                val argTypes: List[Type] = method.args.map(._2).map(analyzeType)

                                // verify the existence of the return type
                                if (verbose) Console.println("    Analyzing return type")
                                var retType: Type = method.ret_type match {
                                    case Some(rt) => analyzeType(rt)
                                    case None => null
                                }

                                // type check finished, insert method into method scope
                                var vsc: VarScope = new VarScope
                                vsc.enter(new VarSymbol(Name("this"), csc.lookup(clazz.name) match {case Some(a) => IClassType(a);case None => INoType}, method.pos))
                                argsIterator = method.args.elements
                                var argTypesIterator = argTypes.elements
                                while (argsIterator.hasNext) {
                                    var arg = argsIterator.next
                                    vsc.enter(new VarSymbol(arg._1, argTypesIterator.next, method.pos))
                                }
                                var msc: MethodScope = csc.lookup(clazz.name) match {
                                    case Some(c) => c.methods
                                    case None => null
                                }
                                // case class MethodSymbol(name: Name, argTypes: List[Type], retType: Type, pos: Int) extends Symbol
                                if (verbose) Console.println("    Entering method " + clazz.name + "." + method.name)
                                if (msc != null) msc.enter(new MethodSymbol(method.name, argTypes, retType, vsc, method.pos)) */
                            }
                        }
                    }
                    case None =>
                }
            }
            case None => { // no inheritance, define method
							msc.lookup(method.name) match {
								case None => {
	                if (verbose) Console.println("    Defining method " + method.name)
	                // verify the existence of the arguments' types and construct the argTypes list
	                //var argsIterator = method.args.elements
	                val argTypes: List[Type] = method.args.map(._2).map(analyzeType)
	            
	                // verify the existence of the return type
	                if (verbose) Console.println("    Analyzing return type")
	                var retType: Type = method.ret_type match {
	                    case Some(rt) => analyzeType(rt)
	                    case None => null
	                }
	                
	                // type check finished, insert variables into variable scope and method into method scope
	                // case class VarSymbol(name:Name, t: Type, pos: Int) extends Symbol
	                var vsc: VarScope = new VarScope
	                vsc.enter(new VarSymbol(Name("this"), csc.lookup(clazz.name) match {case Some(a) => IClassType(a);case None => INoType}, method.pos, 0))
	                

	                var argsIterator = method.args.elements
	                var argTypesIterator = argTypes.elements
	                while (argsIterator.hasNext) {
	                    var arg = argsIterator.next
	                    vsc.enter(new VarSymbol(arg._1, argTypesIterator.next, method.pos, 0))
	                }
	                var msc: MethodScope = csc.lookup(clazz.name) match {
	                    case Some(c) => c.methods
	                    case None => new MethodScope()
	                }
	                if (verbose) Console.println("    Entering method " + clazz.name + "." + method.name)
	                if (msc != null) msc.enter(new MethodSymbol(method.name, argTypes, retType, vsc, method.pos))
								}
								case Some(a) => {
									Report.error(a.pos, "Method " + a.name + " already defined in the same class");
								}
							}
            }
        }
    }
    
    def analyzeMethodBodies(classes: List[ClassDef]): Unit = {
        var classesIterator = classes.elements
        while (classesIterator.hasNext) {
            var classDef: ClassDef = classesIterator.next
            var membersIterator = classDef.membres.elements
            while (membersIterator.hasNext) {
                var member = membersIterator.next
                member match {
                    case m: MethodDef => {
                        var msc: MethodScope = csc.lookup(classDef.name) match {
                            case Some(classSymbol) => classSymbol.methods
                            case None => new MethodScope()
                        }
                        var fsc: FieldScope = csc.lookup(classDef.name) match {
                            case Some(classSymbol) => classSymbol.fields
                            case None => new FieldScope()
                        }
                        
                        var vsc: VarScope = msc.lookup(m.name) match {
                            case Some(methodSymbol) => methodSymbol.variables
                            case None => new VarScope()
                        }
                        analyzeExpression(m.body, vsc)
                    }
                    case f: FieldDecl => // Do nothing
                }
            }
        }
    }
    
    def analyzeExpression(expr: Expr, vsc: VarScope): Type = {
        expr match {
            case eIdent: Ident => {
                vsc.lookup(eIdent.name) match {
                    case Some(varSymbol)=> varSymbol.t
                    case None=> {
                        Report.error(eIdent.pos, "Unknown identifier: " + eIdent.name)
                        IBadType
                    }
                }
            }
            case eNew: New => {
                csc.lookup(eNew.name) match {
                    case Some(classSymbol) => {
                        if (!isSubType(eNew.exprs.map(t => analyzeExpression(t, vsc)), classSymbol.fields.fieldScope.values.toList.map(.t))) IBadType
                        else {
                            var newType: Type = IClassType(classSymbol)
                            newType
                        }
                    }
                    case None => Report.error(eNew.pos, "new: Unknown class"); IBadType
                }
            }
            case Select(expr, name) => {
                analyzeExpression(expr, vsc) match {
                    case IClassType(clazz) => {
                      clazz.fields.lookup(name) match{
                        case Some(f) => return f.t
                        case None => Report.error(expr.pos, "Field " + name + " is not member of class "+ clazz.name); IBadType
                        }
                      }
                      case others => Report.error(expr.pos, "select: Unknown class"); IBadType
                    }
                    IBadType
                  }
                  case Call(expr, name, exprs) => {
                    analyzeExpression(expr, vsc) match {
                      case IClassType(clazz) => {
                        clazz.methods.lookup(name) match{
                          case Some(m) => {
                            if(m.argTypes.length != exprs.length) Report.error(expr.pos, "Not the same number of arguments")
                              if ( List.forall2(m.argTypes, exprs)((a: Type,b: Expr) => isSubType(analyzeExpression(b, vsc), a)) ){
                            }
                            return m.retType;
                          }
                          case None => Report.error(expr.pos, "method " + name + " is not member of class " + clazz.name); IBadType
                        }
                      }
                      case others => Report.error(expr.pos, "call: Unknown class"); IBadType
                    }
                    IBadType
                  }
            case eIntLit: IntLit => IIntType
            case eNullLit: NullLit => INoType
            case eUnop: Unop => isIntType(analyzeExpression(eUnop.expr, vsc))
            case eBinop: Binop => {
                if ((eBinop.op == Eq) || (eBinop.op == Ne)) {
                    if (isSubType(analyzeExpression(eBinop.left, vsc), analyzeExpression(eBinop.right, vsc)) || isSubType(analyzeExpression(eBinop.right, vsc), analyzeExpression(eBinop.left, vsc))) {
                        IIntType
                    } else {
                        Report.error(eBinop.pos, "Object comparison: object types do not agree.")
                        IBadType
                    }
                } else {
                    isIntType(analyzeExpression(eBinop.left, vsc))
                    isIntType(analyzeExpression(eBinop.right, vsc))
                }
            }
            case eReadInt: ReadInt => IIntType
            case eReadChar: ReadChar => IIntType
            case Block(stats, return_value) =>{
              stats.foreach((s: Stat) => analyzeStatement(s, vsc) );
              return_value match{
                case Some(e) => analyzeExpression(e, vsc)
                case None => INoType
              }
              IBadType
            }
          }
        }
    
    def analyzeStatement(s: Stat, vsc: VarScope): VarScope = s match{
        case While(cond, stats) => {
          var vscmod = vsc; //create a copy of the variable scope to be able to modify it
          isIntType(analyzeExpression(cond, vscmod));
          stats.foreach((s: Stat) => vscmod = analyzeStatement(s, vscmod) );
          vscmod
        }
        case If(cond, iftrue, iffalse) =>{
          var vscmod = vsc;
          isIntType(analyzeExpression(cond, vscmod));
          vscmod = analyzeStatement(iftrue, vscmod);
          vscmod = analyzeStatement(iffalse, vscmod);
          vscmod
        }
        case v: Var =>{
          var vscmod = vsc;
          if (!(isSubType(analyzeExpression(v.init,vsc), analyzeType(v.vartype)))){
            Report.error(v.pos, "Var: trying to assign value of different variable type");
          }
          vscmod.lookup(v.varname) match{
            case Some(a) => Report.error(v.pos, "Var: " + v.varname  + " already exists");
            case None => vscmod.enter(VarSymbol(v.varname, analyzeType(v.vartype), v.pos, 0));
          }
          vscmod
        }
        case Set(name, expr) =>{
          vsc.lookup(name) match{
            case Some(a) =>{
              if (!(isSubType(analyzeExpression(expr, vsc), a.t))){
                Report.error(expr.pos, "Set: incompatible types in assignment");
              }
            }
            case None => Report.error(s.pos, "Set: type " + name + " does not exist")
          }
          vsc
        }
        case Do(expr) =>{
          analyzeExpression(expr, vsc);
          vsc
        }
        case PrintInt(expr) =>{
          isIntType(analyzeExpression(expr, vsc));
          vsc
        }
        case PrintChar(expr) =>{
          isIntType(analyzeExpression(expr, vsc));
          vsc
        }
        case Compound(stats) =>{
          var vscmod = vsc; //create a copy of the variable scope to be able to modify it
          stats.foreach((s: Stat) => vscmod = analyzeStatement(s, vscmod) );
          vscmod
        }
        case others => vsc;
    }
    
    // isSubType returns true if a <: b
    def isSubType(a: Type, b: Type): boolean = {
        a match {
            case IIntType => {
                b match {
                    case IIntType => true
                    case INoType => false
                    case IClassType(classB) => false
                }
            }
            case INoType => {
                b match {
                    case IIntType => false
                    case INoType => true
                    case IClassType(classB) => false
                }
            }
            case IClassType(classA) => {
                b match {
                    case IIntType => false
                    case INoType => false
                    case IClassType(classB) => ((classA.parents.contains(classB.name)) || (classA == classB))
                }
            }
						case null => {
								b match {
										case null => true
										case others => false
								}
						}
            case IBadType => true
        }
    }
    
    // isSubType for lists of types
    def isSubType(a: List[Type], b: List[Type]): boolean = {
        if (a.length != b.length) {
            Report.error("Not the same number of arguments.")
            false
        } else {
            List.forall2(a, b)(isSubType)
        }
    }
    
    def analyzeType(typeTree: TypeTree):Type = {
        typeTree match {
            case IntType() => IIntType
            case NullType() => INoType
            case ClassType(name) => {
                csc.lookup(name) match {
                    case Some(a) => IClassType(a)
                    case None => Report.error("Type "+name+" not found"); IBadType;
                }
            }
        }
    }
    
    def isIntType(t: Type): Type = {
        t match {
            case IIntType => IIntType
            case _ => Report.error("integer expected."); IBadType
        }
    }
}
